test(runner): pin the suite's timezone to UTC - #8486
Conversation
The date-face pin family asserts literal LOCAL-date faces (`Jul 4, 2024`, `7/4/2024 7:00 am`) built from fixed UTC instants through local date parts, and nothing pinned the runner's zone. Measured over the six files carrying the family at 76573a1: green at UTC, 7 failures at Europe/Paris, 7 at Asia/Shanghai, 31 at America/New_York, 33 at Etc/GMT+8 — green at exactly one offset, invisible on CI (which is UTC) and paid entirely by contributors elsewhere. `vitest.config.mts` now sets `process.env.TZ = 'UTC'` in module scope, which `apps/console/vitest.config.ts` inherits by importing it. The literal faces stay: they are deliberate anti-tautology anchors, and deriving them from the formatter under test would turn them into `formatDate(v) === formatDate(v)`. `scripts/__tests__/vitest-timezone-pin-8366.test.ts` spawns a real vitest under `TZ=Etc/GMT+8` and asserts the run still sees UTC, so deleting the pin reds CI rather than only a contributor's laptop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
|
One re-run taken, and it is the only one this PR gets.
That is the identical case and the identical numbers that removed PR #8462 from the merge queue at 01:10:28Z — a PR whose entire diff was two comment blocks — and it passed on the same base for PR #8461 in between, then passed for #8462 on re-queue. This PR's diff is a ⇒ Filed as objectui#8493 (unlabelled, unassigned — the repair lands in ⛔ Nothing about this branch changes for it: no rebase, no force-push, no edit. If the re-run is red on the same case, that is the second failure and this PR stops here rather than spending another cycle — the card would then be a live blocker rather than an intermittent one, and this seat would say so on objectui#8493 instead of re-running again. Generated by Claude Code |
|
The re-run failed on the same case with the same numbers: Why the earlier attribution was wrong, stated plainlyAt 02:31Z this seat wrote that the failure was "⛔ not this PR's", on the strength of one occurrence and the argument that "the failing assertion counts kanban cards drawn from a
⇒ At the same base, a PR that does not touch ⭐ The likelier reading, and why it may make this PR more valuable rather than lessTwo things can both be true: an intermittent failure exists independent of this branch (#8462 failed and #8461 passed on one base), and this diff converts it into a deterministic one. Pinning
What happens to this PR⛔ Not closed, ⛔ not rebased, ⛔ not re-run again. It stays open at objectui#8493 is updated with the same correction — ⛔ its "non-deterministic" framing was written before the second failure and no longer describes the evidence. Generated by Claude Code |
|
The mechanism this card declined to name, measured — and a re-scope: ⛔ not "intermittent, cause unknown". ⭐
|
|
Unblocked — The blocker is gone, measured on
|
|
LANDED — ⭐ This PR sat Landing probe, on a re-fetched
|
| probe | expect | read |
|---|---|---|
process.env.TZ in vitest.config.mts |
> 0 | 2 |
'UTC' in the same file |
> 0 | 1 |
scripts/__tests__/vitest-timezone-pin-8366.test.ts present in the tree |
present | ✔ (git ls-tree) |
⭐ firing control — defineConfig in the same file, same command shape |
> 0 | 3 |
⭐ nonsense control — zzq-not-a-real-token-8366 |
0 | 0 |
⇒ The zero is a reading: the same command returns 3 on a term certainly present in the same file.
What is now on main
The suite's timezone is pinned to UTC at the runner level, with a spawn-based guard test that fails if the pin is removed — so the date-face family of assertions no longer depends on the ambient zone of whatever machine runs them.
⚠️ One thing this card's history is worth keeping for
This card is the first of three today whose PR was dequeued or held by ObjectKanban.contractEnvelope-6839.test.tsx — a case in a package none of the three diffs touches. The seat's first instinct on it was wrong and is on the record: a rule reading "two failures on the same head is a property of that head" was written, acted on, produced a retracted attribution on this very PR, and was then withdrawn. What replaced it — ⭐ attribution is structural: what settles ownership is whether the diff can reach the failing code — is the rule that then held for #8511, for #8538's Bundle Analysis red, and for this PR through six hours of being blocked.
Generated by Claude Code
Fixes #8366
The defect, re-measured on
origin/main(76573a184)The date-face pin family asserts literal local-date faces built from fixed
UTC instants through local date parts. Nothing pinned the runner's zone, so
the face those pins render was a property of the contributor's laptop.
All six files the card names, run together, one run per zone:
TZUTCEurope/ParisAsia/ShanghaiAmerica/New_YorkEtc/GMT+8Two corrections to the card
says the suite is green at EXACTLY ONE offset. It is red at
Europe/Parisand
Asia/Shanghaitoo — east of UTC, not west of anything. A face likeJul 4, 2024, 07:00 AM, pinned off an07:00Zinstant, is true at UTC+00:00and nowhere else. Sample failures:
expected 'Jul 3, 2024, 11:00 PM' to be 'Jul 4, 2024, 07:00 AM',expected '7/3/2024 11:00 pm' to be '7/4/2024 7:00 am'.10 failed | 4 passed (14)at base
411a132c0; on76573a184the same file reads11 failed | 4 passed (15)— PR test(core): pin the OUT-of-window precedence pair formatDate(V, 'relative', { style: 'short' }) #8353 added a row. The defect is unchanged; only the count is.
The card's zeros reproduce exactly, each against a lit control:
TZ/timeZone= 0 invitest.config.mts,turbo.json,package.jsonandall six
vitest.setup.*;TZ:= 0 workflows. Controls:'environment'invitest.config.mts= 8;runs-on= 34 workflows.The asymmetry the card asked to have explained
dataset-format.date.test.tsandDatasetWidget.dateMeasure.test.tsxpass inevery zone, and it is not luck. Both derive the expected face by calling the
same formatter the code under test calls, so the zone cancels:
DatasetWidget.dateMeasure.test.tsxsays so in a docblock headed "Why theexpectations are DERIVED and not literal" — and it was chosen for a different
reason entirely (a literal would pass just as well against a second date
convention, the objectui#4576 failure its ruling forbids).
Why the zone and not the assertions — the fork, argued
The four failing files pin literals on purpose, and
date-display.optionsStyle-7745.test.tsstates the purpose in prose: its facesare "anchored by the two literals the card measured, so a redesign that moved
both sides together could not pass silently." Those literals sit next to
equivalence assertions of the form
formatDate(V, undefined, { style }) === formatDate(V, style), which arezone-independent already and which a redesign could move on both sides at once.
The literals are the anti-tautology anchor.
So rewriting them to derive the face is not a refactor — it turns the anchor
into
formatDate(v) === formatDate(v)and deletes the thing the file waswritten for. This repo already uses each form where it belongs; the fix should
not flatten them into one.
Chosen: pin the runner's zone. One line, in the one place that already owns
how this repo runs tests:
apps/console/vitest.config.tsimports the root config, so its module scope runsfor that project too; the 11 remaining
packages/*/vite.config.tsreachassertCanonicalVitestInvocation, which refuses the package-cwd invocations thatcould bypass it. Measured: with the pin, all six files are
184 passedunderEtc/GMT+8,America/New_York,Asia/ShanghaiandUTC.Unconditional, not
??=. A contributor's zone usually comes from/etc/localtime, not fromTZ, so a conditional assignment would leave exactlythe reported population unfixed while looking like a fix. The guard test forbids
the
??=/||=spelling for that reason.It deletes no coverage — measured, not assumed
The card asks whether pinning hides a real zone bug. It does not:
process.env.TZ,getTimezoneOffset,Intl.DateTimeFormat().resolvedOptions()return 0 hitsacross every
*.test.ts/*.test.tsx. Live control: the sameresolvedOptions()pattern returns 5 hits elsewhere in the tree, so the grepfires.
GanttView.tsx'smakeTzShift/tzOffsetMs, takes its zone from metadata (schema.timeZone), andGanttView.tzshift.test.tsalready tests it with explicit per-case zones(
Asia/Shanghai,America/New_York, a DST pair). That is exactly the shapethe card says non-UTC coverage should have, and it is unaffected.
Costs of the options not taken
pays for it by converting four files' deliberate literal anchors into
self-comparisons.
fields-date-widget-convention-8194.test.tsxalone carries16 failing cases whose expected faces would become tautologies.
that would still be zone-dependent". That document has no consumer and no
gate; the useful part of it is the guard below plus the asymmetry explanation
above, both of which ship here.
The pin is pinned —
scripts/__tests__/vitest-timezone-pin-8366.test.tsCI runs in UTC. So an in-process
getTimezoneOffset() === 0would be green on CIwhether or not the config still pins anything — it would catch a deleted pin only
for the contributor it protects, and stay silent where merges are gated. That is
the original defect one level up.
So the guard spawns a real vitest with
TZ=Etc/GMT+8and asserts the runstill sees UTC. Four cases: the ambient reading; a
node -elive control provingTZactually reaches a child (without it, a broken env hand-off would make thespawn pass forever); the spawn itself; and a static half that names the file and
forbids the conditional spelling.
Ablation — direction predicted in writing before the run
Predicted: the spawn case and the static case go RED; the ambient case stays
GREEN (this box is UTC — which is the point), and the control stays green.
Mutation proven on disk before reading any result:
Observed, matching the prediction:
and inside the spawned child, visible in the failure output — the direct reading
that the pin is what forces UTC:
Restored under an
EXIT INT TERMtrap with absolute paths, viagit checkout HEAD -- ..., and proven by state:git diff HEAD --statprinted nothing.
Files touched outside
scripts/Per the card's scope fence — this fix touches no other lane's test file.
The four red files are fixed by the runner, not edited.
vitest.config.mts— the pin plus its comment. Contents only: thecheck-lint-rule-coverageUNREACHED_GROUPSrow that names this exact path(objectui#8337, PR tooling(devx): report source files ESLint reaches for NEITHER reason #8467) is untouched, and
check:lint-rule-coverageis green.Independently confirmed by measurement: the file appears 0 times in
lint:root's 276-file population, which is what that row asserts..changeset/8366-vitest-timezone-pin.md— empty frontmatter. Testinfrastructure; no package is released by this change.
Verification
184 passedin every onepnpm exec vitest run scripts/122 passed (122),3634 passed (3634)— includes every test that readsvitest.config.mts2 failed | 2 passed (4), matching the written prediction; restore proven bygit diff HEADemptynode scripts/check-changeset-presence.mjsnode scripts/check-lint-rule-coverage.mjsnode scripts/check-governed-queue-guard.mjs --test ...check:control-bytes·check:vi-mock-inherit·check:vi-mock-specifiers·check:unreferenced-sourcestype-check:scripts·type-check:coverage·type-check:vitest-configLint, as a proven narrowing (at
e1641f8e6, the final commit):lint:root— the repo's own root-lane invocation, so the receiving population isread from ESLint's own config, not guessed — judged 276 files, 0 errors, 32
warnings, exit 0, counted from
--format json. The new test file is in thatpopulation with 0/0. Invariance for the
packages/*/apps/*lanes thisnarrows away:
eslint.config.jscontains 0 occurrences ofprojectService,parserOptionsorproject:(control: 10 occurrences ofrulesin the samerun), so no type-aware linting is enabled and this diff cannot move the verdict
on any file it did not touch — and it touches no file in those lanes.
Type-check is measured, not assumed:
tsc -p tsconfig.scripts.json --listFilesreturns the new test file, so
type-check:scriptsreally compiled it.Generated by Claude Code